﻿using System;
using System.Runtime.InteropServices;

namespace Timing
{
    public class TimingHelper
    {
        [DllImport("Kernel32.dll")]
        private static extern bool QueryPerformanceCounter(out long lpPerformanceCount);

        [DllImport("Kernel32.dll")]
        private static extern bool QueryPerformanceFrequency(out long lpFrequency);

        protected TimingTreeNode TimingTreeRoot;

        protected TimingTreeNode LastEndedNode;

        protected TimingTreeNode CurrentNode;

        public TimingTreeNode TimingTree
        {
            get { return this.TimingTreeRoot; }
        }

        public TimingHelper()
        {
            this.TimingTreeRoot = new TimingTreeNode("Root");
            this.CurrentNode = this.TimingTreeRoot;
        }

        public void TimingStart(string name)
        {
            TimingTreeNode node = new TimingTreeNode(name);
            long temp;

            //If the current node hasn't already been started, start it.
            if (!this.CurrentNode.IsStarted)
            {
                TimingHelper.QueryPerformanceCounter(out temp);
                this.CurrentNode.Start = temp;
            }

            //Check to see if the current node has ended, if so, throw
            //Timings can potentially be inaccurate, tree setup might be inaccurate
            if (this.CurrentNode.IsEnded)
            {
                throw new Exception("Current node has already been ended.  Timings and format may be inaccurate.");
            }

            //set the parent, and add the node to the child list
            node.Parent = this.CurrentNode;
            this.CurrentNode.Children.Add(node);

            //now this is the current node
            this.CurrentNode = node;
            TimingHelper.QueryPerformanceCounter(out temp);
            node.Start = temp;
        }

        public void TimingEnd()
        {
            if (this.CurrentNode != null)
            {
                //this closes out the current node, and moves us up the tree one step
                long temp = 0;
                TimingHelper.QueryPerformanceCounter(out temp);
                if (!this.CurrentNode.IsEnded)
                {
                    this.CurrentNode.End = temp;
                    this.LastEndedNode = this.CurrentNode;
                    if (this.CurrentNode.Parent != null)
                    {
                        this.CurrentNode = this.CurrentNode.Parent;
                    }
                }
                else
                {
                    //
                    throw (new Exception("Too many Ends called, root already ended."));
                }
            }
            else
            {
                //Should not have happened, but throw a meaningful exception
                throw (new Exception("The current Timing Node is Null"));
            }

        }

        public decimal GetLastTime()
        {
            return this.LastEndedNode.TotalSeconds;
        }

        public class TimingTreeNode
        {
            private long StartTime;
            private long EndTime;
            private bool wasForcedEnd;
            protected string name = "";
            protected TimingTreeNodeList children = new TimingTreeNodeList();

            /// <summary>
            /// Indicator for whether this node has been ended
            /// </summary>
            public bool IsEnded { get; set; }

            /// <summary>
            /// Indicator for whether this node has been started
            /// </summary>
            public bool IsStarted { get; set; }

            /// <summary>
            /// Set the end time and flag IsEnded, or get the end time
            /// </summary>
            public long End
            {
                get { return this.EndTime; }
                set
                {
                    this.EndTime = value;
                    this.IsEnded = true;
                }
            }

            /// <summary>
            /// Set the start time and flag IsStarted, or get the start time
            /// </summary>
            public long Start
            {
                get { return this.StartTime; }
                set
                {
                    this.StartTime = value;
                    this.IsStarted = true;
                }
            }

            /// <summary>
            /// Calculate the total time between start and end time
            /// </summary>
            public decimal TotalSeconds
            {
                get
                {
                    long freq = 0;
                    TimingHelper.QueryPerformanceFrequency(out freq);

                    return (EndTime - StartTime) * (decimal)1.0 / freq;
                }
            }

            public TimingTreeNodeList Children
            {
                get { return this.children; }
            }

            [System.Xml.Serialization.XmlIgnore()]
            public TimingTreeNode Parent { get; set; }

            public string Name
            {
                get { return this.name; }
            }

            public TimingTreeNode(string name)
            {
                this.name = name;
            }

            /// <summary>
            /// Ends all nodes, and displays the timing tree
            /// </summary>
            /// <param name="recursive">Display children</param>
            /// <returns>Formatted string for the current Node and children, with timings and names</returns>
            public string ToString(bool recursive)
            {
                return this.ToString(0, recursive);
            }

            /// <summary>
            /// Ends all nodes, and displays the timing tree
            /// </summary>
            /// <returns>Formatted string for the current Node and children, with timings and names</returns>
            public override string ToString()
            {
                return this.ToString(true);
            }


            /// <summary>
            /// Ends all nodes, and displays the timing tree
            /// </summary>
            /// <param name="space">Number of tabs for this node's output</param>
            /// <param name="recursive">Display this node's children</param>
            /// <returns>Formatted string for the current Node and children, with timings and names</returns>
            private string ToString(int space, bool recursive)
            {
                System.Text.StringBuilder retVal = new System.Text.StringBuilder();

                if (!this.IsEnded)
                {
                    this.EndRemainingNodes();
                }

                retVal.Append('\t', space);
                if (this.children.Count > 0)
                    retVal.AppendFormat("{0}{1}:\t{2:#0.000}s, # of Children: {3} - average per sub process: {4:#0.000}, {5}", this.name, (this.wasForcedEnd ? "*" : ""), this.TotalSeconds, this.children.Count.ToString(), this.TotalSeconds / this.children.Count, (this.Parent == null ? 1 : (this.TotalSeconds / this.Parent.TotalSeconds)).ToString("P"));
                else
                    retVal.AppendFormat("{0}{1}:\t{2:#0.000}s, {3}", this.name, (this.wasForcedEnd ? "*" : ""), this.TotalSeconds, (this.Parent == null ? 1 : (this.TotalSeconds / this.Parent.TotalSeconds)).ToString("P"));
                if (recursive)
                {
                    foreach (TimingTreeNode oChild in this.children)
                    {
                        retVal.Append("\r\n");
                        retVal.Append(oChild.ToString(space + 1, true));
                    }
                }

                return retVal.ToString();
            }


            /// <summary>
            /// End node and all sub-nodes
            /// </summary>
            private void EndRemainingNodes()
            {
                TimingTreeNode child;

                //we can't count the children until the sub-nodes are all closed, 
                // but we only have to check the last child for isEnded.
                if (this.children.Count > 0)
                {
                    child = this.children[this.children.Count - 1];
                    if (!child.IsEnded)
                    {
                        child.EndRemainingNodes();
                    }
                }

                if (!this.IsEnded)
                {
                    long temp = 0;
                    TimingHelper.QueryPerformanceCounter(out temp);
                    this.End = temp;
                    this.wasForcedEnd = true;
                }
            }

        }


        [Serializable()]
        public class TimingTreeNodeList : System.Collections.CollectionBase
        {

            public TimingTreeNode this[int index]
            {
                get { return (TimingTreeNode)this.List[index]; }
                set { this.List[index] = value; }
            }

            public int Add(TimingTreeNode value)
            {
                return this.List.Add(value);
            }

            public void Insert(int index, TimingTreeNode value)
            {
                this.List.Insert(index, value);
            }

            public int IndexOf(TimingTreeNode value)
            {
                return this.List.IndexOf(value);
            }

            public bool Contains(TimingTreeNode value)
            {
                return this.List.Contains(value);
            }

            public void Remove(TimingTreeNode value)
            {
                this.List.Remove(value);
            }

            public void CopyTo(TimingTreeNode[] array, int index)
            {
                this.List.CopyTo(array, index);
            }
        }

    }
}
